package test.spring;

//import java.beans.IntrospectionException;
//import java.beans.Introspector;
//import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.ClassUtils;


public abstract class BaseClassScanner<T> {

	protected final Log logger = LogFactory.getLog(getClass());

	private final List<TypeFilter> includeFilters = new LinkedList<TypeFilter>();
	private final List<TypeFilter> excludeFilters = new LinkedList<TypeFilter>();

	private MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
	

	public BaseClassScanner() {
		registerDefaultFilters();
	}

	protected void addIncludeFilter(TypeFilter includeFilter) {
		this.includeFilters.add(includeFilter);
	}

	protected void addExcludeFilter(TypeFilter excludeFilter) {
		this.excludeFilters.add(0, excludeFilter);
	}

	protected void registerDefaultFilters() {
		addIncludeFilter(new TypeFilter() {
			public boolean match(MetadataReader metadataReader,
					MetadataReaderFactory metadataReaderFactory)
					throws IOException {
				return true;
			}
		});

		// exclude package-info.java
		addExcludeFilter(new TypeFilter() {
			public boolean match(MetadataReader metadataReader,
					MetadataReaderFactory metadataReaderFactory)
					throws IOException {
				String className = metadataReader.getClassMetadata()
						.getClassName();
				return className.endsWith("package-info");
			}
		});
	}

	protected boolean isCandidateComponent(MetadataReader metadataReader)
			throws IOException {
		for (TypeFilter tf : this.excludeFilters) {
			if (tf.match(metadataReader, this.metadataReaderFactory)) {
				return false;
			}
		}
		for (TypeFilter tf : this.includeFilters) {
			if (tf.match(metadataReader, this.metadataReaderFactory)) {
				return true;
			}
		}
		return false;
	}

	protected T getCandidateComponent(MetadataReader metadataReader) {
		ClassMetadata metadata = metadataReader.getAnnotationMetadata();

		if (!metadata.isConcrete() || !metadata.isIndependent()) {
			return null;
		}

		Class<?> clazz;
		try {
			clazz = Class.forName(metadata.getClassName());
		} catch (ClassNotFoundException e) {
			logger.warn("ClassNotFoundException occurs", e);
			return null;
		} catch (Throwable e) {
			logger.warn("ClassNotFoundException occurs", e);
			return null;
		}
		
		return getCandidateComponent(metadata, clazz);
	}

	protected abstract T getCandidateComponent(ClassMetadata metadata,
			Class<?> clazz);

	public Set<T> findCandidateComponents(String... basePackages) {
		Set<T> candidates = new LinkedHashSet<T>();
		for (String basePackage : basePackages) {
			candidates.addAll(findCandidateComponents(basePackage));
		}
		return candidates;
	}
	

	/**
	 * Scan the class path for candidate components.
	 * 
	 * @param basePackage
	 *            the package to check for annotated classes
	 * @return a corresponding Set of autodetected bean definitions
	 */
	protected Set<T> findCandidateComponents(String basePackage) {
		Set<T> candidates = new LinkedHashSet<T>();
		try {

			Resource[] resources = ResourceUtil
					.getResourcesOfClasses(basePackage);

			for (Resource resource : resources) {

				logger.trace("Scanning " + resource);
				
				if (resource.isReadable()) {
					try {
						MetadataReader metadataReader = this.metadataReaderFactory
								.getMetadataReader(resource);
						if (isCandidateComponent(metadataReader)) {
							T component = getCandidateComponent(metadataReader);
							if (component != null) {
								logger.debug("Identified candidate component class: "
										+ resource);
								candidates.add(component);
								
							} else {
								logger.debug("Ignored because not a concrete top-level class: "
										+ resource);
							}
						} else {
							logger.trace("Ignored because not matching any filter: "
									+ resource);
						}
					} catch (Throwable ex) {
//						logger.error("Failed to read candidate component class: "
//										+ resource, ex);
						throw new RuntimeException(
								"Failed to read candidate component class: "
										+ resource, ex);
					}
				} else {
					logger.trace("Ignored because not readable: " + resource);
				}
			}
		} catch (IOException ex) {
			throw new RuntimeException("I/O failure during classpath scanning",
					ex);
		}
		return candidates;
	}

	
	/**
	 * 
	 * @see org.springframework.core.io.Resource
	 * @author yuqih
	 * 
	 */
	private static class ResourceUtil {

		private static final ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();

		public static final String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
//		public static final String CLASSPATH_URL_PREFIX = "classpath:";
//		public static final String FILE_URL_PREFIX = "file:";
		public static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";


		/**
		 * @param packageSearchPath
		 * @return
		 * @throws IOException
		 */
		static Resource[] getResourcesOfClasses(String basePackage)
				throws IOException {
			String packageSearchPath = CLASSPATH_ALL_URL_PREFIX
					+ resolveBasePackage(basePackage) + "/"
					+ DEFAULT_RESOURCE_PATTERN;
			return resolver.getResources(packageSearchPath);
		}

		/**
		 * @param basePackage
		 * @return
		 */
		static String resolveBasePackage(String basePackage) {
			return ClassUtils.convertClassNameToResourcePath(basePackage);
		}

	}

}
